package com.jp.fm.service;

import java.text.SimpleDateFormat;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Calendar;
import java.util.Collections;
import java.util.Date;
import java.util.GregorianCalendar;
import java.util.HashMap;
import java.util.Iterator;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import java.util.Map.Entry;
import java.util.Set;
import java.util.TreeMap;

import javax.servlet.http.HttpServletRequest;

import org.apache.commons.lang3.math.NumberUtils;

import com.jp.fm.HomeFinanceRunner;
import com.jp.fm.dao.AccountDao;
import com.jp.fm.dao.TransactionDao;
import com.jp.fm.data.Account;
import com.jp.fm.data.AccountType;
import com.jp.fm.data.Transaction;

public class PageService {

	private static final int NET_INCOME_FIRST_MONTH = 0;
	private static final int NET_INCOME_LAST_MONTH = 5;
	private static final int NET_INCOME_MAX_MONTHS = 6;
	private static final int HOURS_TO_REMOVE_ACCOUNT_ALERT = 72;

	private static final float ONE_HUNDRED_PERCENT = 100;
	private static final float ALERT_MAX_CREDIT_USED_PERCENTAGE = 50;

	private AccountDao accountDao = null;
	private TransactionDao transactionDao = null;

	private AccountService accountService = null;

	public void composeRequestLeftNav(final HttpServletRequest request) {

		// Lookup accounts and add them to the response.
		Map<AccountType, List<Account>> accountsByType = HomeFinanceRunner
				.getAccountsByType();

		Map<String, Account> accountsWithErrors = new HashMap<String, Account>();

		// Remove any hidden accounts and populate the accountsWithErrors List
		Set<Entry<AccountType, List<Account>>> entrySet = accountsByType
				.entrySet();
		for (Map.Entry<AccountType, List<Account>> entry : entrySet) {

			List<Account> accountListUpdated = accountService
					.removeHiddenAccounts(entry.getValue());

			// Populate accountsWithErrors
			for (Account account : accountListUpdated) {
				if (recentAccountRefreshFailure(account)) {
					accountsWithErrors.put(account.getName(), account);
				}
			}

			if (accountListUpdated.size() != entry.getValue().size()) {
				request.setAttribute("hiddenAccounts", "true");
				entry.setValue(accountListUpdated);
				continue;
			}
		}

		// If an account type is now empty then we need to remove it also
		Map<AccountType, List<Account>> accountsByTypeUpdated = new TreeMap<AccountType, List<Account>>(
				accountsByType);
		Set<AccountType> keys = accountsByType.keySet();
		for (AccountType type : keys) {
			if (accountsByType.get(type).size() == 0) {
				accountsByTypeUpdated.remove(type);
			}
		}

		request.setAttribute("accountsWithErrors", accountsWithErrors);
		request.setAttribute("accountsByType", accountsByTypeUpdated);

		String[] opens = request.getParameterValues("o");
		if (null != opens) {
			request.setAttribute("o", Arrays.asList(opens));
		}
	}

	public void composeRequestNetWorth(final HttpServletRequest request) {
		Float net = NumberUtils.FLOAT_ZERO;
		Float assets = NumberUtils.FLOAT_ZERO;
		Float debt = NumberUtils.FLOAT_ZERO;
		Float accountBal = NumberUtils.FLOAT_ZERO;

		List<Account> accounts = this.accountDao.findAll();
		for (Account account : accounts) {
			accountBal = account.getBalance();
			net += accountBal;
			if (accountBal < 0) {
				debt += accountBal;
			}
			if (accountBal > 0) {
				assets += accountBal;
			}
		}

		request.setAttribute("assets", assets);
		request.setAttribute("debt", debt);
		request.setAttribute("net", net);
	}

	public void composeRequestNetIncome(final HttpServletRequest request) {

		List<Map<String, Object>> trendList = new ArrayList<Map<String, Object>>();
		SimpleDateFormat sdf = new SimpleDateFormat("MM/dd/yyyy");
		Map<String, Object> map = null;
		Float monthTranTotal = null;
		Float spentTotal = Float.valueOf("0");
		Float incomeTotal = Float.valueOf("0");
		Float net = Float.valueOf("0");
		Float monthMax = Float.valueOf("0");
		Date now = new Date();
		Calendar cal = new GregorianCalendar();
		cal.setTime(now);
		for (int i = 0; i < NET_INCOME_MAX_MONTHS; i++) {

			map = new HashMap<String, Object>();

			if (i == NET_INCOME_LAST_MONTH) {
				request.setAttribute("monthLongStart", cal.getDisplayName(
						Calendar.MONTH, Calendar.LONG, Locale.getDefault()));
				request.setAttribute("monthTopCatsStart",
						sdf.format(cal.getTime()));
			}

			if (i == NET_INCOME_FIRST_MONTH) {
				request.setAttribute("monthLongEnd", cal.getDisplayName(
						Calendar.MONTH, Calendar.LONG, Locale.getDefault()));
				request.setAttribute("monthTopCatsEnd", sdf.format(now));
			}

			monthTranTotal = Float.valueOf("0");

			map = new HashMap<String, Object>();

			map.put("monthLong",
					cal.getDisplayName(Calendar.MONTH, Calendar.LONG,
							Locale.getDefault()));

			map.put("monthShort",
					cal.getDisplayName(Calendar.MONTH, Calendar.SHORT,
							Locale.getDefault()));

			Calendar cal2 = new GregorianCalendar();
			cal2.setTime(cal.getTime());

			// Get start date
			cal2.set(Calendar.DAY_OF_MONTH,
					cal2.getMinimum(Calendar.DAY_OF_MONTH));
			Date startDate = cal2.getTime();
			map.put("monthStartDate", sdf.format(startDate));

			// Get the end date
			cal2.set(Calendar.DAY_OF_MONTH,
					cal2.getMaximum(Calendar.DAY_OF_MONTH));
			Date endDate = cal2.getTime();
			map.put("monthEndDate", sdf.format(endDate));

			// Get all transactions for the month
			List<Transaction> trans = transactionDao.findByAny(null, startDate,
					endDate, null, null, null);
			for (Iterator<Transaction> iterator = trans.iterator(); iterator
					.hasNext();) {
				Transaction transaction = iterator.next();

				// Get the account and determine if we need to add or subtract
				// amount
				Account acct = accountDao.findByName(transaction.getAccount());
				// Only include credit, checking and savings account
				// transactions
				if ("Credit Cards".equals(acct.getType().getName())
						|| "Checking".equals(acct.getType().getName())
						|| "Savings".equals(acct.getType().getName())) {
					Float amt = transaction.getAmount();
					if ("Credit Cards".equals(acct.getType().getName())) {
						amt *= -1;
					}

					if (amt > 0) {
						incomeTotal += amt;
					}
					if (amt < 0) {
						spentTotal += amt;
					}

					monthTranTotal += amt;
				}
			}

			map.put("monthTranTotal", monthTranTotal);

			if (Math.abs(monthMax) < Math.abs(monthTranTotal)) {
				monthMax = monthTranTotal;
			}

			net += monthTranTotal;

			trendList.add(map);

			cal.set(Calendar.DAY_OF_MONTH,
					cal.getMinimum(Calendar.DAY_OF_MONTH));
			cal.add(Calendar.MONTH, -1);
		}

		Collections.reverse(trendList);

		request.setAttribute("trendList", trendList);
		request.setAttribute("sixMonthIncome", incomeTotal);
		request.setAttribute("sixMonthSpent", Math.abs(spentTotal));
		request.setAttribute("sixMonthNet", net);
		request.setAttribute("monthMax", monthMax);
	}

	public void composeAccountBalancesOverTime(
			final HttpServletRequest request, final int numMonths) {

		Map<String, List<Float>> accountAverageBalMap = new HashMap<String, List<Float>>();

		List<String> balanceDateList = new ArrayList<String>();
		List<Float> balanceList = null;
		Float aveBal = null;
		Date now = new Date();
		Calendar cal = new GregorianCalendar();
		cal.setTime(now);
		cal.set(Calendar.DAY_OF_MONTH, cal.getMinimum(Calendar.DAY_OF_MONTH));
		cal.add(Calendar.MONTH, -numMonths);
		List<Account> accountList = accountDao.findAll();

		// Remove any hidden accounts
		List<Account> accountListUpdated = accountService
				.removeHiddenAccounts(accountList);

		for (int i = 0; i < numMonths; i++) {

			balanceDateList.add(cal.getDisplayName(Calendar.MONTH,
					Calendar.SHORT, Locale.getDefault()) + " 1");

			balanceDateList.add(cal.getDisplayName(Calendar.MONTH,
					Calendar.SHORT, Locale.getDefault()) + " 15");

			for (Account account : accountListUpdated) {

				if ("Credit Cards".equals(account.getType().getName())
						|| "Checking".equals(account.getType().getName())
						|| "Savings".equals(account.getType().getName())) {

					balanceList = accountAverageBalMap.get(account.getName());
					if (null == balanceList) {
						balanceList = new ArrayList<Float>();
						accountAverageBalMap
								.put(account.getName(), balanceList);
					}

					Calendar cal2 = new GregorianCalendar();
					cal2.setTime(cal.getTime());

					// Set the first half of the month
					// Get start date
					cal2.set(Calendar.DAY_OF_MONTH,
							cal2.getMinimum(Calendar.DAY_OF_MONTH));
					Date startDate = cal2.getTime();
					// Get the end date
					cal2.set(Calendar.DAY_OF_MONTH, 15);
					Date endDate = cal2.getTime();

					aveBal = accountDao.findAverageAccountBalanceForRange(
							startDate, endDate, account);
					aveBal = Math.abs(aveBal);
					balanceList.add(aveBal);

					// Set the second half of the month
					// Get start date
					cal2.set(Calendar.DAY_OF_MONTH, 16);
					startDate = cal2.getTime();
					// Get the end date
					cal2.set(Calendar.DAY_OF_MONTH,
							cal2.getMaximum(Calendar.DAY_OF_MONTH));
					endDate = cal2.getTime();

					aveBal = accountDao.findAverageAccountBalanceForRange(
							startDate, endDate, account);
					aveBal = Math.abs(aveBal);
					balanceList.add(aveBal);
				}
			}

			cal.set(Calendar.DAY_OF_MONTH,
					cal.getMinimum(Calendar.DAY_OF_MONTH));
			cal.add(Calendar.MONTH, 1);
		}

		request.setAttribute("accountAverageBalMap", accountAverageBalMap);
		request.setAttribute("balanceMonthList", balanceDateList);
	}

	/**
	 * Finds account refresh failures.
	 * @return List of alerts
	 */
	public List<String> findAccountAlerts() {
		List<String> ret = new ArrayList<String>();
		SimpleDateFormat sdf = new SimpleDateFormat("MM/dd/yyyy hh:mm:ss aa");

		List<Account> accts = accountDao.findAll();

		for (Account account : accts) {
			if (recentAccountRefreshFailure(account)) {
				ret.add("Account refresh failure on "
						+ sdf.format(account.getLastFailureDate())
						+ " for account " + account.getName());
			}
		}

		return ret;
	}

	public boolean recentAccountRefreshFailure(final Account account) {

		boolean ret = false;

		if (null != account.getLastFailureDate()
				&& HomeFinanceRunner.hoursBetween(account.getLastFailureDate(),
						new Date()) < HOURS_TO_REMOVE_ACCOUNT_ALERT) {
			ret = true;
		}

		return ret;
	}

	/**
	 * Look for alerts having to do with credit cards. Includes: 1. Total credit
	 * usage > 50%
	 * @return List of alerts
	 */
	public List<String> findCreditAlerts() {
		List<String> ret = new ArrayList<String>();
		List<Account> accts = accountDao.findByType(AccountType
				.findByName("Credit Cards"));
		Float totalCreditUsed = NumberUtils.FLOAT_ZERO;
		Float totalCredit = NumberUtils.FLOAT_ZERO;
		for (Account account : accts) {
			totalCreditUsed += (account.getTotalCredit() - account
					.getAvailableCredit());
			totalCredit += account.getTotalCredit();
		}

		Float percentUsed = totalCreditUsed / totalCredit;
		Float fiftyPercent = ALERT_MAX_CREDIT_USED_PERCENTAGE
				/ ONE_HUNDRED_PERCENT;

		if (percentUsed > fiftyPercent) {
			ret.add("Total credit used is above 50%!");
		}

		return ret;
	}

	public void setAccountDao(final AccountDao accountDao) {
		this.accountDao = accountDao;
	}

	public void setTransactionDao(final TransactionDao transactionDao) {
		this.transactionDao = transactionDao;
	}

	public final void setAccountService(final AccountService accountService) {
		this.accountService = accountService;
	}
}
